<?php

/*
Author: Sun Yi Ming (dolmens `@` gmail.com)
MultiCurl是一个基于回调编程模型的并行curl封装，以简化编程
同时，由于是在得到结果之后，立即执行回调函数，理论上使得可以更快地执行逻辑
*/

/**
 * class that represent a single curl request
 * user should extends this class to provide your own process function
 */
class MultiCurlRequest {
	protected $url;
	protected $postData;
	protected $options;
	
	/**
     * @param string $url
     * @param string $postData if present, the library will post this param
     * @param array $options curl_setopt_array params
     * @return void
     */
	public function __construct($url, $postData = null, $options = null) {
		$this->url = $url;
		$this->postData = $postData;
		$this->options = $options;
	}
	
	public function __get($property) {
		if (isset($this->$property)) {
			return $this->$property;
		} else {
			return null;
		}
	}
	
	/**
     * @param int $resultCode 0 is ok
     * @param array $info 请求底层对应的curl handle的info array
     * @param string $content 结果字符串
     * @return void
     */
	public function process($resultCode, $info, $content) {
	}
};

class MultiCurlChannel {
	private $multi;
	private $request;
	private $retry;
	private $ch;
	
	public function __construct($multi, $request, $retry = 0) {
		$this->multi = $multi;
		$this->request = $request;
		$this->retry = $retry;
		$this->ch = null;
	}
	
	public function stop() {
		$this->multi->removeRequestChannel($this);
	}
	
	public function __get($property) {
		if (isset($this->$property)) {
			return $this->$property;
		} else {
			return null;
		}
	}
	
	public function __set($property, $value) {
		$this->$property = $value;
	}
};

class MultiCurl {
	private $mh;
	private $channelMap;
	private $options;
	private $requestAdded; // 是否有新request添加
	
	/**
     * @param array $options curl_setopt_array params，缺省用在所有的curl request中。CURLOPT_RETURNTRANSFER被自动设置
     * @return void
     */
	public function __construct($options = null) {
		$this->mh = curl_multi_init();
		$this->channelMap = array();
		$this->options = $options;
		$this->requestAdded = false;
	}
	
	public function __destruct() {
		curl_multi_close($this->mh);
	}
	
	public function __get($property) {
		if ($property == 'options')
			return $this->options;
		else
			return null;
	}
	
	/**
     * @param MultiCurlRequest $request
	 * @param retry 重试次数
     * @return MultiCurlChannel
     */
	public function addRequest($request, $retry = 0) {
		$channel = new MultiCurlChannel($this, $request, $retry);
		$this->addRequestChannel($channel);
		
		return $channel;
	}
	
	public function addRequestChannel($channel) {
		$request = $channel->request;
		
		$ch = curl_init();
		
		$options = array(
			CURLOPT_URL => $request->url,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_NOSIGNAL => 1
			);
		if ($request->options) {
			$options += $request->options;
		}
		if ($this->options) {
			$options += $this->options;
		}
		if ($request->postData) {
			$options[CURLOPT_POSTFIELDS] = $request->postData;
		}
		curl_setopt_array($ch, $options);
		
		curl_multi_add_handle($this->mh, $ch);
		
		$channel->ch = $ch;
		$this->channelMap[$ch] = $channel;
		$this->requestAdded = true;
	}
	
	/**
     * @param MultiCurlChannel $channel
     * @return void
     */
	public function removeRequestChannel($channel) {
		curl_multi_remove_handle($this->mh, $channel->ch);
		unset($this->channelMap[$channel->ch]);
	}
	
	public function execute() {
		while (true) {
			// echo "execute<br/>\n";
			do {
				$mrc = curl_multi_exec($this->mh, $active);
			} while ($mrc == CURLM_CALL_MULTI_PERFORM);
			
			$this->requestAdded = false;
			
			if ($mrc == CURLM_OK) {
				do {
					$msgcnt = 0;
					$msg = curl_multi_info_read($this->mh, $msgcnt);
					if ($msg !== false) {
						// echo "got info from multi stack<br/>\n";
						$ch = $msg['handle'];
						$info = curl_getinfo($ch);
						$content = curl_multi_getcontent($ch);

						curl_multi_remove_handle($this->mh, $ch);
						
						$channel = $this->channelMap[$ch];
						unset($this->channelMap[$ch]);
						
						$request = $channel->request;
						if ($msg['result'] == 0 || $channel->retry == 0) {
							$request->process($msg['result'], $info, $content);
						} else {
							$channel->retry--;
							$this->addRequestChannel($channel);
						}
					}
				} while ($msgcnt > 0);
			} else {
				// something wrong, cannot recover
				return -1;
			}

			// 如果有新添加的request，则不需要select
			if (!$this->requestAdded) {
				// 如果没有新添加的request，且active为0，则推出循环
				if ($active < 1)
					break;
				curl_multi_select($this->mh, 0.5);
			}
		}
    }
	
	/**
	 * abort MultiCurl execute loop
	 */
	public function finish() {
		foreach ($this->channelMap as $ch => $channel) {
			curl_multi_remove_handle($this->mh, $channel->ch);
		}
		$this->channelMap = array();
	}
};
